/************************************************************************
* wm_x11.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#define _WM_EXPOSE_ALL
#include "wm.h"
#include "graphics.h"

#ifndef WIN32

#include <X11/Xatom.h>

#define GLX_CONTEXT_MAJOR_VERSION_ARB       0x2091
#define GLX_CONTEXT_MINOR_VERSION_ARB       0x2092
typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);

/* attributes for a double buffered visual in RGBA format with at least
 * 4 bits per color and a 16 bit depth buffer */
static int attr_list[] = {
	GLX_RENDER_TYPE, GLX_RGBA_BIT,
	GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
	GLX_DOUBLEBUFFER, True,
	GLX_X_RENDERABLE, True,
	GLX_RED_SIZE, 4,
	GLX_GREEN_SIZE, 4,
	GLX_BLUE_SIZE, 4,
	GLX_ALPHA_SIZE, 4,
	GLX_DEPTH_SIZE, 16,
	None
};

static XIMStyle style_shoose_best(XIMStyle style1, XIMStyle style2)
{
	XIMStyle s,t;
	XIMStyle preedit = XIMPreeditArea | XIMPreeditCallbacks |
	XIMPreeditPosition | XIMPreeditNothing | XIMPreeditNone;
	XIMStyle status = XIMStatusArea | XIMStatusCallbacks |
	XIMStatusNothing | XIMStatusNone;

	if (style1 == 0)
		return style2;
	if (style2 == 0)
		return style1;
	if ((style1 & (preedit | status)) == (style2 & (preedit | status)))
		return style1;

	s = style1 & preedit;
	t = style2 & preedit;
	if (s != t) {
		if (s | t | XIMPreeditCallbacks) {
			return (s == XIMPreeditCallbacks)?style1:style2;
		}else if (s | t | XIMPreeditPosition) {
			return (s == XIMPreeditPosition)?style1:style2;
		}else if (s | t | XIMPreeditArea) {
			return (s == XIMPreeditArea)?style1:style2;
		}else if (s | t | XIMPreeditNothing) {
			return (s == XIMPreeditNothing)?style1:style2;
		}
	}else{ /* if preedit flags are the same, compare status flags */
		s = style1 & status;
		t = style2 & status;
		if (s | t | XIMStatusCallbacks) {
			return (s == XIMStatusCallbacks)?style1:style2;
		}else if (s | t | XIMStatusArea) {
			return (s == XIMStatusArea)?style1:style2;
		}else if (s | t | XIMStatusNothing) {
			return (s == XIMStatusNothing)?style1:style2;
		}
	}
	return style1;
}

/* initialise the game window */
int wm_init()
{
	int min;
	int max;
	char* glXv;

	wm_data.isinit = 1;

	/* get a connection */
	wm_data.dpy = XOpenDisplay(0);
	wm_data.screen = DefaultScreen(wm_data.dpy);
	wm_data.fb_cfg = NULL;
	wm_data.cursor.mat = NULL;

	wm_data.lfps_i = 0;
	wm_data.lfps[0] = 0;
	wm_data.lfps[1] = 0;
	wm_data.lfps[2] = 0;
	wm_data.lfps[3] = 0;

	if (XSetLocaleModifiers("") == NULL)
		return 1;

	/* get a connection */
	XF86VidModeQueryVersion(wm_data.dpy, &max, &min);
	vlprintf(CN_INFO, "XF86VidModeExtension version: %d.%d", max, min);
	XF86VidModeGetAllModeLines(wm_data.dpy, wm_data.screen, &wm_data.mode_count, &wm_data.modes);

	/* save desktop resolution before switching modes */
	wm_data.deskMode = *wm_data.modes[0];

	glXv = (char*)glXGetClientString(wm_data.dpy, GLX_VERSION);
	vlprintf(CN_INFO, "glX version: %s",glXv,max,min);

	return wm_create();
}

/* exit the game window */
void wm_exit()
{
	wm_destroy();
	XFree(wm_data.modes);
	XCloseDisplay(wm_data.dpy);
}

/* create a window */
int wm_create()
{
	Colormap cmap;
	Atom wmDelete;
	Window winDummy;
	unsigned int borderDummy;
	int x;
	int y;
	unsigned int w;
	unsigned int h;
	unsigned int d;
	int i;
	glXCreateContextAttribsARBProc glXCreateContextAttribsARB = 0;
	XIMStyles *im_supported_styles;
	XIMStyle app_supported_styles;
	XIMStyle style;
	XVaNestedList list;
	int im_event_mask;

	if (!wm_data.fb_cfg) {
		GLXFBConfig *fb_configs = NULL;
		int num_fb_configs = 0;

		fb_configs = glXChooseFBConfig(wm_data.dpy, wm_data.screen, attr_list, &num_fb_configs);
		if (!fb_configs || !num_fb_configs) {
			fb_configs = glXGetFBConfigs(wm_data.dpy, wm_data.screen, &num_fb_configs);
			if (!fb_configs || !num_fb_configs)
				return 1;
		}
		wm_data.fb_cfg = fb_configs[0];
		glXGetFBConfigAttrib(wm_data.dpy, wm_data.fb_cfg, GLX_DOUBLEBUFFER, &wm_data.dblbuff);
		wm_data.vi = glXGetVisualFromFBConfig(wm_data.dpy, wm_data.fb_cfg);

		glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddressARB( (const GLubyte *) "glXCreateContextAttribsARB" );

		if (!glXCreateContextAttribsARB) {
			wm_data.ctx = glXCreateNewContext(wm_data.dpy, wm_data.fb_cfg, GLX_RGBA_TYPE, 0, True);
		}else{
			int context_attribs[] = {
				GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
				GLX_CONTEXT_MINOR_VERSION_ARB, 3,
				GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
				GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
				None
			};
			wm_data.ctx = glXCreateContextAttribsARB(wm_data.dpy, wm_data.fb_cfg, 0, True, context_attribs);
		}
		if (!wm_data.ctx)
			return 1;
	}

	/* set best mode to current */
	wm_data.mode = 0;
	/* look for mode with requested resolution */
	for (i=0; i<wm_data.mode_count; i++) {
		if ((wm_data.modes[i]->hdisplay == wm_data.size.width) && (wm_data.modes[i]->vdisplay == wm_data.size.height)) {
			wm_data.mode = i;
			break;
		}
	}

	/* create a color map */
	cmap = XCreateColormap(
		wm_data.dpy,
		RootWindow(wm_data.dpy, wm_data.vi->screen),
		wm_data.vi->visual,
		AllocNone
	);

	wm_data.attr.colormap = cmap;
	wm_data.attr.border_pixel = 0;
	wm_data.attr.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask;

	if (wm_data.fullscreen) {
		XF86VidModeSwitchToMode(wm_data.dpy, wm_data.screen, wm_data.modes[wm_data.mode]);
		XF86VidModeSetViewPort(wm_data.dpy, wm_data.screen, 0, 0);

		/* create a fullscreen window */
		wm_data.attr.override_redirect = True;
		wm_data.win = XCreateWindow(
			wm_data.dpy,
			RootWindow(wm_data.dpy, wm_data.vi->screen),
			0,
			0,
			wm_data.modes[wm_data.mode]->hdisplay,
			wm_data.modes[wm_data.mode]->vdisplay,
			0,
			wm_data.vi->depth,
			InputOutput,
			wm_data.vi->visual,
			CWBorderPixel | CWColormap | CWEventMask | CWOverrideRedirect,
			&wm_data.attr
		);
	}else{
		/* create a window */
		wm_data.win = XCreateWindow(
			wm_data.dpy,
			RootWindow(wm_data.dpy, wm_data.vi->screen),
			0,
			0,
			wm_data.size.width,
			wm_data.size.height,
			0,
			wm_data.vi->depth,
			InputOutput,
			wm_data.vi->visual,
			CWBorderPixel | CWColormap | CWEventMask,
			&wm_data.attr
		);

		/* handle wm_delete_events */
		wmDelete = XInternAtom(wm_data.dpy, "WM_DELETE_WINDOW", True);
		XSetWMProtocols(wm_data.dpy, wm_data.win, &wmDelete, 1);
	}

	wm_data.im = XOpenIM(wm_data.dpy, NULL, NULL, NULL);
	if (wm_data.im == NULL)
		return 1;

	app_supported_styles = XIMPreeditNone | XIMPreeditNothing | XIMPreeditArea;
	app_supported_styles |= XIMStatusNone | XIMStatusNothing | XIMStatusArea;

	/* figure out which styles the IM can support */
	XGetIMValues(wm_data.im, XNQueryInputStyle, &im_supported_styles, NULL);

	/* find the best IM */
	wm_data.style = 0;
	for(i=0; i < im_supported_styles->count_styles; i++) {
		style = im_supported_styles->supported_styles[i];
		if ((style & app_supported_styles) == style) /* if we can handle it */
			wm_data.style = style_shoose_best(style, wm_data.style);
	}


	/* ... or not */
	if (wm_data.style == 0)
		return 1;

	/* create an IC from the IM, this is needed for unicode support */
	list = XVaCreateNestedList(0,NULL);
	wm_data.ic = XCreateIC(
		wm_data.im,
		XNInputStyle, wm_data.style,
		XNClientWindow, wm_data.win,
		NULL
	);
	XFree(list);

	if (wm_data.ic == NULL)
		return 1;

	XGetICValues(wm_data.ic, XNFilterEvents, &im_event_mask, NULL);
	XSelectInput(wm_data.dpy, wm_data.win, ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask | im_event_mask);

	XSetICFocus(wm_data.ic);

	if (wm_data.fullscreen) {
		XWarpPointer(wm_data.dpy, None, wm_data.win, 0, 0, 0, 0, 0, 0);
		XMapRaised(wm_data.dpy, wm_data.win);
		XGrabKeyboard(wm_data.dpy, wm_data.win, True, GrabModeAsync, GrabModeAsync, CurrentTime);
		XGrabPointer(wm_data.dpy, wm_data.win, True, ButtonPressMask, GrabModeAsync, GrabModeAsync, wm_data.win, None, CurrentTime);
	}else{
		XSizeHints *hints;
		XMapRaised(wm_data.dpy, wm_data.win);

		/* set window title */
		XStoreName(wm_data.dpy, wm_data.win, wm_data.title);

		/* prevent window resizing */
		hints = XAllocSizeHints();
		hints->min_width = hints->max_width = wm_data.size.width;
		hints->min_height = hints->max_height = wm_data.size.height;
		hints->flags = PMinSize | PMaxSize;
		XSetWMNormalHints(wm_data.dpy, wm_data.win, hints);
		XFree(hints);
	}

	wm_data.glxwin = glXCreateWindow(wm_data.dpy, wm_data.fb_cfg, wm_data.win, NULL);

	/* make OpenGL context current */
	if (!glXMakeContextCurrent(wm_data.dpy, wm_data.glxwin, wm_data.glxwin, wm_data.ctx)) {
		glXDestroyContext(wm_data.dpy, wm_data.ctx);
		return 1;
	}

	XGetGeometry(
		wm_data.dpy,
		wm_data.win,
		&winDummy,
		&x,
		&y,
		&w,
		&h,
		&borderDummy,
		&d
	);

	wm_data.size.width = (int)w;
	wm_data.size.height = (int)h;

	if (glXIsDirect(wm_data.dpy, wm_data.ctx)) {
		int major;
		int minor;
		char* db = "";
		glGetIntegerv(GL_MAJOR_VERSION, &major);
		glGetIntegerv(GL_MINOR_VERSION, &minor);

		if (wm_data.dblbuff)
			db = "Double Buffered ";

		vlprintf(CN_INFO, "%sDirect Rendering: OpenGL %d.%d",db,major,minor);
	}

	/* MUST be done, else rendering gets messed up */
	render_set_projection_matrix(NULL);

	return 0;
}

/* resize the screen, fullscreen or windowed */
int wm_resize()
{
	if (!wm_data.isinit)
		return 0;

	XResizeWindow(wm_data.dpy, wm_data.win, wm_data.size.width, wm_data.size.height);

	/* MUST be done after a resize, else rendering gets messed up */
	render_set_projection_matrix(NULL);

	return 0;
}

/* flush graphics through and flip buffers */
int wm_update()
{
	glFlush();
	/* update the screen */
	if (wm_data.dblbuff)
		glXSwapBuffers(wm_data.dpy, wm_data.glxwin);
	return 0;
}

/* destroy the current window */
void wm_destroy()
{
	if (wm_data.ctx) {
		glXMakeContextCurrent(wm_data.dpy, None, None, NULL);

		glXDestroyContext(wm_data.dpy, wm_data.ctx);
		wm_data.ctx = NULL;
		glXDestroyWindow(wm_data.dpy,wm_data.glxwin);
	}
	/* switch back to original desktop resolution if we were in fs */
	if (wm_data.fullscreen) {
		XF86VidModeSwitchToMode(wm_data.dpy, wm_data.screen, &wm_data.deskMode);
		XF86VidModeSetViewPort(wm_data.dpy, wm_data.screen, 0, 0);
	}

}

/* set fullscreen on/off */
void wm_toggle_fullscreen(int fs)
{
	if (fs == wm_data.fullscreen)
		return;

	if (!wm_data.isinit) {
		wm_data.fullscreen = fs;
		return;
	}
	if (wm_data.ctx) {
		glXMakeContextCurrent(wm_data.dpy, None, None, NULL);
		glXDestroyWindow(wm_data.dpy,wm_data.glxwin);
	}
	/* switch back to original desktop resolution if we were in fs */
	if (wm_data.fullscreen) {
		XF86VidModeSwitchToMode(wm_data.dpy, wm_data.screen, &wm_data.deskMode);
		XF86VidModeSetViewPort(wm_data.dpy, wm_data.screen, 0, 0);
	}
	wm_data.fullscreen = fs;
	/* TODO: this isn't working in glX */
	wm_create();
}

/* use file as a cursor texture */
void wm_cursor(char* file, int width, int height, int offset_x, int offset_y)
{
	Cursor invisibleCursor;
	Pixmap bitmapNoData;
	XColor black;
	static char noData[] = { 0,0,0,0,0,0,0,0 };

	if (!file) {
		if (!wm_data.cursor.mat)
			return;

		wm_data.cursor.mat = NULL;
		XUndefineCursor(wm_data.dpy, wm_data.win);

		return;
	}

	wm_data.cursor.mat = mat_from_image("texture",file);
	wm_data.cursor.w = width;
	wm_data.cursor.h = height;
	wm_data.cursor.x = offset_x;
	wm_data.cursor.y = offset_y;

	if (!wm_data.cursor.mat)
		return;

	black.red = 0;
	black.green = 0;
	black.blue = 0;

	bitmapNoData = XCreateBitmapFromData(wm_data.dpy, wm_data.win, noData, 8, 8);
	invisibleCursor = XCreatePixmapCursor(wm_data.dpy, bitmapNoData, bitmapNoData, &black, &black, 0, 0);
	XDefineCursor(wm_data.dpy, wm_data.win, invisibleCursor);
	XFreeCursor(wm_data.dpy, invisibleCursor);
}

/* grab the mouse */
void wm_grab()
{
	Cursor invisibleCursor;
	Pixmap bitmapNoData;
	XColor black;
	static char noData[] = { 0,0,0,0,0,0,0,0 };

	if (events_get_mousegrab())
		return;

	black.red = 0;
	black.green = 0;
	black.blue = 0;

	bitmapNoData = XCreateBitmapFromData(wm_data.dpy, wm_data.win, noData, 8, 8);
	invisibleCursor = XCreatePixmapCursor(wm_data.dpy, bitmapNoData, bitmapNoData, &black, &black, 0, 0);
	XDefineCursor(wm_data.dpy, wm_data.win, invisibleCursor);
	XFreeCursor(wm_data.dpy, invisibleCursor);

	if (!wm_data.fullscreen)
		XGrabPointer(wm_data.dpy, wm_data.win, True, ButtonPressMask, GrabModeAsync, GrabModeAsync, wm_data.win, None, CurrentTime);

	events_set_mousegrab(1);
}

/* stop grabbing the mouse */
void wm_ungrab()
{
	if (!events_get_mousegrab())
		return;

	XUndefineCursor(wm_data.dpy, wm_data.win);
	if (!wm_data.fullscreen)
		XUngrabPointer(wm_data.dpy, CurrentTime);

	events_set_mousegrab(0);
}

/* set the window title */
void wm_title(char* title)
{
	if (title) {
		if (wm_data.title)
			free(wm_data.title);
		wm_data.title = strdup(title);
	}
	if (!wm_data.isinit)
		return;

	XStoreName(wm_data.dpy, wm_data.win, wm_data.title);
}

#endif
